"""RDFshow template parser."""

import StringIO

import rdflib

import template

__all__ = ['LexError', 'ParseError']


# The parser gets tokens from the lexer. Each token is a 2-tuple
# (tok_type, tok_value) where tok_type is a unicode identifying the token type,
# and tok_value is its value or None if it has no value. Special types 'str'
# and 'word' represent arbitrary strings and latin-alphanumeric words,
# respectively; they have values, other types don't.


class LexError(Exception):
    """Indicates that a lexical error has prevented lexing template code."""
    pass


class TemplateLexer(object):
    """Template code lexer."""
    def __init__(self, code):
        """Create a lexer for the specified unicode `code`."""
        self.code = code
    
    def lex(self):
        """Lex the code and return a list of tokens.
        
        Raise `LexError` if the code is lexically invalid.
        """
        # This lexer is basically a finite state machine.
        # Its transition table has the format:
        #   {current_state: [(input_char, new_state, action)]}
        # If input_char is a single character, it must match exactly.
        # If input_char is the magic string "any", it matches any input
        # character. Otherwise, input_char is a function check_char(c) which
        # returns True if the transition should happen. action, if not None, is
        # called after the transition.
        
        buf = StringIO.StringIO()
        tok = []
        
        def make_tok(type_):
            # Produces a token of specified type from the current buffer.
            if type_ in (u'str', u'word'):
                tok.append((type_, buf.getvalue()))
                # Clear the buffer
                buf.seek(0)
                buf.truncate()
            else:
                tok.append((type_, None))
        
        # Action definitions.
        def append():            buf.write(c)
        def not_open_tag():      buf.write(u'<' + c)
        def slash():             make_tok(u'/')
        def equals():            make_tok(u'=')
        def quote_start():       make_tok(u'"')
        def not_end_comment():   buf.write('-' + c)
        def close_tag():         make_tok(u'@>')
        def open_tag():
            if buf.pos > 0:
                make_tok(u'str')
            make_tok(u'<@')
        def comment_mark():
            if buf.pos > 0:
                make_tok(u'str')
            make_tok(u'--')
        def word_equals():
            make_tok(u'word')
            make_tok(u'=')
        def word():
            make_tok(u'word')
        def quote_end():
            make_tok(u'str')
            make_tok(u'"')
        
        table = {
            'null': [
                (u'<', 'open', None),
                (u'any', 'null', append)
            ], 'open': [
                (u'@', 'tag', open_tag),
                (u'<', 'open', append),
                (u'any', 'null', not_open_tag)
            ], 'tag': [
                (u'/', 'tag', slash),
                (unicode.isalnum, 'word', append),
                (u'=', 'tag', equals),
                (u'"', 'quoted-str', quote_start),
                (u'-', 'begin-comment', None),
                (u'@', 'close', None),
                (unicode.isspace, 'tag', None)
            ], 'begin-comment': [
                (u'-', 'comment', comment_mark)
            ], 'comment': [
                (u'-', 'end-comment', None),
                (u'any', 'comment', append)
            ], 'end-comment': [
                (u'-', 'tag', comment_mark),
                (u'any', 'comment', not_end_comment)
            ], 'word': [
                (unicode.isalnum, 'word', append),
                (u'-', 'word', append),
                (u'=', 'tag', word_equals),
                (unicode.isspace, 'tag', word),
                (u'@', 'close', word)
            ], 'quoted-str': [
                (u'"', 'tag', quote_end),
                (u'any', 'quoted-str', append)
            ], 'close': [
                (u'>', 'null', close_tag)
            ]
        }
        
        state = 'null'
        linenum = 1
        colnum = 1
        for c in self.code:
            # Select new state from candidates
            new_state = None
            action = None
            for inp, candidate, act in table[state]:
                if (callable(inp) and inp(c)) or (inp == c) or (inp == u'any'):
                    new_state = candidate
                    action = act
                    break
            if new_state is None:
                raise LexError('unexpected char %s at line %d, column %d' %
                               (c, linenum, colnum))
            state = new_state
            if action is not None:
                action()
            # Move line/column pointer
            if c == u'\n':
                linenum += 1
                colnum = 1
            else:
                colnum += 1
        # Make last token
        if buf.pos > 0:
            make_tok(u'str')
        return tok


class ParseError(Exception):
    """Indicates that a syntactic or semantic error has prevented parsing."""
    pass


class TemplateParser(object):
    """Template code parser."""
    # This is a fairly simple recursive descent parser. Methods whose names
    # begin with _d_ (such as _d_elem) represent derivations. Methods whose
    # names begin with _e_ (such as _e_node_id) represent functions for
    # making element objects from attribute dictionaries.
    
    _EMPTY_ELEMS = (u'prefix', u'node-id', u'literal-val', u'node-count',
                    u'stmt-count', u'render-node-set', u'render-node',
                    u'node-id-sha1', u'literal-lang', u'render-stmt-set',
                    u'render-stmt')
    """Tuple containing names of all empty elements in the language."""
    
    def __init__(self, code):
        """Create a parser for the specified unicode `code`."""
        self.code = code
        self.tpls = []
        self.prefixes = {}
    
    def _eat(self, type_=None):
        """Eat the current token of type `type_` and return its value.
        
        If `type_` is None, eat any type of token. Otherwise, if the current
        token's type is not `type_`, raise `ParseError`.
        """
        if self.i >= len(self.tokens):
            raise ParseError(u'end of code reached while %s was expected' %
                             type_)
        tok_type, tok_value = self.tokens[self.i]
        if (type_ is not None) and (tok_type != type_):
            raise ParseError(u'got %s, expected %s' % (tok_type, type_))
        self.i += 1
        return tok_value
    
    def _peek(self, j=0):
        """Return the type of the token `j` places from the current.
        
        If no such token exists, return None.
        """
        if (self.i + j) >= len(self.tokens):
            return None
        else:
            return self.tokens[self.i + j][0]
    
    def _expand_refs(self, refs, single=False, placeholder=False):
        """Expand a reference list attribute into a list of RDFLib nodes.
        
        Assume that `refs` is a list of whitespace-separated URIs, CURIEs or
        blank node references, and convert it into a Python list of
        corresponding node objects, using `self`.prefixes to expand CURIEs.
        If `single` is True, then assume that `refs` only contains one URI,
        CURIE or blank node ref (and do not treat whitespace specially).
        If `placeholder` is True, then allow the special string "@node" among
        the references, expanding to the "current node" placeholder (as defined
        by `template.CURRENT_NODE`.
        """
        # Helper function to do the actual expansion
        def expand(ref):
            if ref == u'@node' and placeholder:
                return template.CURRENT_NODE
            if u':' in ref:
                # Possible CURIE or blank node.
                prefix, rest = ref.split(u':')
                if prefix == u'_':
                    return rdflib.BNode(rest)
                elif prefix in self.prefixes:
                    return rdflib.URIRef(self.prefixes[prefix] + rest)
                else:
                    return rdflib.URIRef(ref)
            else:
                return rdflib.URIRef(ref)
        
        if single:
            return expand(refs)
        else:
            return [expand(ref) for ref in refs.split()]
    
    def allow_attrs(allowed):
        """Decorator to ensure that only `allowed` attributes are present."""
        def decorator(fun):
            # Infer the name of the element from the method's name.
            elem_name = fun.__name__[3:].replace('_', '-')
            def new_fun(self, attrs, children):
                for attr in attrs:
                    if attr not in allowed:
                        raise ParseError(
                            u'unknown attribute "%s" in element %s' %
                            (attr, elem_name))
                return fun(self, attrs, children)
            return new_fun
        return decorator
    
    def _make_text(self, text):
        """Build and return a text element with specified `text`."""
        return template.TextElem(text)
    
    def _make_elem(self, name, attrs, children=None):
        """Build and return a `name` element with `attrs` and `children`."""
        # Locate the method that builds elements of this type.
        name = name.replace(u'-', u'_')
        if hasattr(self, '_e_' + name):
            maker = getattr(self, '_e_' + name)
            return maker(attrs, children)
        else:
            raise ParseError(u'unknown element %s' % name)
    
    @allow_attrs((u'type', u'for', u'flavour'))
    def _e_template(self, attrs, children):
        if u'type' not in attrs:
            raise ParseError(u'no attribute "type" in template element')
        types = {
            u'node_set': template.Template.NODE_SET,
            u'node': template.Template.NODE,
            u'stmt_set': template.Template.STMT_SET,
            u'stmt': template.Template.STMT
        }
        if attrs[u'type'] not in types:
            raise ParseError(u'unknown template type %s' % attrs[u'type'])
        type_ = types[attrs[u'type']]
        if (attrs[u'type'] in (u'node', u'stmt')) and (u'for' in attrs):
            for_nodes = self._expand_refs(attrs[u'for'])
        else:
            for_nodes = None
        flavour = attrs.get(u'flavour')
        return template.Template(children, type_, flavour, for_nodes)
    
    @allow_attrs(())
    def _e_for_node(self, attrs, children):
        return template.ForNodeElem(children)
    
    @allow_attrs((u'escape'))
    def _e_node_id(self, attrs, children):
        escape = not ((u'escape' in attrs) and (attrs[u'escape'] == u'no'))
        return template.NodeIdElem(escape)
    
    @allow_attrs((u'subjects', u'predicates', u'objects', u'lang', u'limit'))
    def _e_select_stmts(self, attrs, children):
        nodes = {}
        for part in (u'subjects', u'predicates', u'objects'):
            if part in attrs:
                nodes[part] = self._expand_refs(attrs[part], placeholder=True)
            else:
                nodes[part] = [None]
        
        if u'lang' in attrs:
            lang = attrs[u'lang'].split()
        else:
            lang = None
        
        if u'limit' in attrs:
            try:
                limit = int(attrs[u'limit'])
            except ValueError:
                raise ParseError(u'invalid select-stmts limit value "%s"' %
                                 attrs[u'limit'])
        else:
            limit = None
        
        return template.SelectStmtsElem(children, nodes[u'subjects'],
                                        nodes[u'predicates'],
                                        nodes[u'objects'], lang, limit)
    
    @allow_attrs((u'subjects', u'predicates', u'objects', u'lang', u'limit',
                  u'remove'))
    def _e_filter_stmts(self, attrs, children):
        nodes = {}
        for part in (u'subjects', u'predicates', u'objects'):
            if part in attrs:
                nodes[part] = self._expand_refs(attrs[part], placeholder=True)
            else:
                nodes[part] = [None]
        
        if u'lang' in attrs:
            lang = attrs[u'lang'].split()
        else:
            lang = None
        
        if u'limit' in attrs:
            try:
                limit = int(attrs[u'limit'])
            except ValueError:
                raise ParseError(u'invalid filter-stmts limit value "%s"' %
                                 attrs[u'limit'])
        else:
            limit = None
        
        remove = ((u'remove' in attrs) and (attrs[u'remove'] == u'yes'))
        
        return template.FilterStmtsElem(children, nodes[u'subjects'],
                                        nodes[u'predicates'],
                                        nodes[u'objects'], lang, limit,
                                        remove)
    
    @allow_attrs(())
    def _e_for_stmt(self, attrs, children):
        return template.ForStmtElem(children)
    
    @allow_attrs((u'escape'))
    def _e_literal_val(self, attrs, children):
        escape = not ((u'escape' in attrs) and (attrs[u'escape'] == u'no'))
        return template.LiteralValElem(escape)
    
    @allow_attrs(())
    def _e_subject(self, attrs, children):
        return template.SubjectElem(children)
    
    @allow_attrs(())
    def _e_predicate(self, attrs, children):
        return template.PredicateElem(children)
    
    @allow_attrs(())
    def _e_object(self, attrs, children):
        return template.ObjectElem(children)
    
    @allow_attrs(())
    def _e_node_count(self, attrs, children):
        return template.NodeCountElem()
    
    @allow_attrs(())
    def _e_stmt_count(self, attrs, children):
        return template.StmtCountElem()
    
    @allow_attrs((u'predicate', u'order', u'lang'))
    def _e_sort_nodes(self, attrs, children):
        if u'predicate' not in attrs:
            raise ParseError(u'no attribute "predicate" in sort-nodes element')
        predicate = self._expand_refs(attrs[u'predicate'], single=True)
        asc = ((u'order' not in attrs) or (attrs[u'order'] != u'desc'))
        lang = attrs.get(u'lang')
        return template.SortNodesElem(children, predicate, asc, lang)
    
    @allow_attrs(())
    def _e_have_nodes(self, attrs, children):
        return template.HaveNodesElem(children)
    
    @allow_attrs(())
    def _e_no_nodes(self, attrs, children):
        return template.NoNodesElem(children)
    
    @allow_attrs((u'flavour'))
    def _e_render_node_set(self, attrs, children):
        return template.RenderNodeSetElem(attrs.get(u'flavour'))
    
    @allow_attrs((u'flavour'))
    def _e_render_node(self, attrs, children):
        return template.RenderNodeElem(attrs.get(u'flavour'))
    
    @allow_attrs(())
    def _e_if_node_resource(self, attrs, children):
        return template.IfNodeResourceElem(children)
    
    @allow_attrs(())
    def _e_if_node_blank(self, attrs, children):
        return template.IfNodeBlankElem(children)
    
    @allow_attrs(())
    def _e_if_node_literal(self, attrs, children):
        return template.IfNodeLiteralElem(children)
    
    @allow_attrs(())
    def _e_node_id_sha1(self, attrs, children):
        return template.NodeIdSha1Elem()
    
    @allow_attrs(())
    def _e_literal_lang(self, attrs, children):
        return template.LiteralLangElem()
    
    @allow_attrs((u'flavour'))
    def _e_render_stmt_set(self, attrs, children):
        return template.RenderStmtSetElem(attrs.get(u'flavour'))
    
    @allow_attrs((u'flavour'))
    def _e_render_stmt(self, attrs, children):
        return template.RenderStmtElem(attrs.get(u'flavour'))
    
    @allow_attrs(())
    def _e_subjects(self, attrs, children):
        return template.SubjectsElem(children)
    
    @allow_attrs(())
    def _e_predicates(self, attrs, children):
        return template.PredicatesElem(children)
    
    @allow_attrs(())
    def _e_objects(self, attrs, children):
        return template.ObjectsElem(children)
    
    @allow_attrs(())
    def _e_have_stmts(self, attrs, children):
        return template.HaveStmtsElem(children)
    
    @allow_attrs(())
    def _e_no_stmts(self, attrs, children):
        return template.NoStmtsElem(children)
    
    @allow_attrs((u'subject', u'predicate', u'object', u'lang'))
    def _e_select_stmt(self, attrs, children):
        nodes = {}
        for part in (u'subject', u'predicate', u'object'):
            if part in attrs:
                nodes[part] = self._expand_refs(attrs[part], single=True,
                                                placeholder=True)
            else:
                nodes[part] = None
        lang = attrs.get(u'lang')
        return template.SelectStmtElem(children, nodes[u'subject'],
                                       nodes[u'predicate'],
                                       nodes[u'object'], lang)
    
    @allow_attrs((u'subject', u'predicate', u'object', u'lang', u'remove'))
    def _e_filter_stmt(self, attrs, children):
        nodes = {}
        for part in (u'subject', u'predicate', u'object'):
            if part in attrs:
                nodes[part] = self._expand_refs(attrs[part], single=True,
                                                placeholder=True)
            else:
                nodes[part] = None
        lang = attrs.get(u'lang')
        remove = ((u'remove' in attrs) and (attrs[u'remove'] == u'yes'))
        return template.FilterStmtElem(children, nodes[u'subject'],
                                       nodes[u'predicate'],
                                       nodes[u'object'], lang, remove)
    
    def _d_S(self):
        while self._peek() == u'str':
            self._eat()
        self._d_elem(self.tpls)
        while self._peek() == u'str':
            self._eat()
        if self._peek() == u'<@':
            self._d_S()
    
    def _d_elem(self, into):
        self._eat(u'<@')
        self._d_elem_kind(into)
    
    def _d_elem_kind(self, into):
        if self._peek() == u'--':
            self._d_comment_elem()
        elif self._peek() == u'word':
            tok_type, tok_value = self.tokens[self.i]
            if tok_value in self._EMPTY_ELEMS:
                self._d_empty_elem(into)
            else:
                self._d_content_elem(into)
        else:
            raise ParseError(u'got %s, expected element beginning' %
                             self._peek())
    
    def _d_empty_elem(self, into):
        elem_name = self._eat(u'word')
        attrs = {}
        if self._peek() == u'word':
            self._d_attrs(attrs)
        self._eat(u'@>')
        if elem_name == u'prefix':
            for prefix in attrs:
                self.prefixes[prefix] = attrs[prefix]
        else:
            into.append(self._make_elem(elem_name, attrs))
    
    def _d_content_elem(self, into):
        elem_name = self._eat(u'word')
        attrs = {}
        if self._peek() == u'word':
            self._d_attrs(attrs)
        self._eat(u'@>')
        children = []
        self._d_body(children)
        self._eat(u'<@')
        self._eat(u'/')
        closing_name = self._eat(u'word')
        if closing_name != elem_name:
            raise ParseError(u'got closing tag %s, expected %s' % \
                             (closing_name, elem_name))
        self._eat(u'@>')
        into.append(self._make_elem(elem_name, attrs, children))
    
    def _d_comment_elem(self):
        self._eat(u'--')
        self._eat(u'str')
        self._eat(u'--')
        self._eat(u'@>')
    
    def _d_attrs(self, into):
        self._d_attr(into)
        if self._peek() == u'word':
            self._d_attrs(into)
    
    def _d_attr(self, into):
        name = self._eat(u'word')
        self._eat(u'=')
        self._eat(u'"')
        value = self._eat(u'str')
        self._eat(u'"')
        into[name] = value
    
    def _d_body(self, into):
        if (self._peek() == u'<@') and (self._peek(1) == u'/'):
            # A closing tag is coming in, body ends here.
            return
        if self._peek() == u'str':
            into.append(self._make_text(self._eat(u'str')))
        else:
            self._d_elem(into)
        self._d_body(into)
    
    def parse(self):
        """Parse the code and return a list of resulting templates.
        
        Raise `ParseError` if the code is syntactically or semantically
        invalid (e.g. invalid element attributes also cause it).
        """
        self.tokens = TemplateLexer(self.code).lex()
        self.i = 0
        self._d_S()
        # Check that all top-level elements are actually Templates
        for tpl in self.tpls:
            if not isinstance(tpl, template.Template):
                raise ParseError(u'only "template" and "prefix" elements '
                                 u'allowed at the top level')
        return self.tpls
